在本教程的这一步骤中,创建一个在其中定义数据源的 Kanzi Engine 插件。数据源用于提供数据给您的应用程序。
本教程的起点资料存储在 <KanziWorkspace>/Tutorials/Data sources/Start 目录中:
<KanziWorkspace>/Tutorials/Data sources/Completed 目录包含本教程已完成的工程。
要定义数据源:
如果您在 Visual Studio 2017 中打开教程解决方案,遇到提示您重新定位工程到最新的 Microsoft 工具集时,请点击取消 (Cancel)。
class XML_DATA_SOURCE_API XMLDataSource : public DataSource { public: KZ_METACLASS_BEGIN(XMLDataSource, DataSource, "CustomDataSourceType") KZ_METACLASS_END() ... };为
class XML_DATA_SOURCE_API XMLDataSource : public DataSource { public: //创建 XmlFilename 属性类型。属性类型可用于告诉数据源插件要读取哪个 XML 文件。 static PropertyType<string> XmlFilenameProperty; KZ_METACLASS_BEGIN(XMLDataSource, DataSource, "XML_data_source") //将您创建的属性添加到类元数据。 KZ_METACLASS_PROPERTY_TYPE(XmlFilenameProperty) KZ_METACLASS_END() ... };
initialize()
函数受保护的部分后声明:parseFile
,用以解析用户选择的 XML 文件。class XML_DATA_SOURCE_API XMLDataSource : public DataSource
{
...
protected:
...
//声明解析 XML 文件的函数,并从其内容创建数据对象。
void parseFile(vector<char> fileData);
//加载应用程序 kzb 文件后,Kanzi 调用 onLoaded。
virtual void onLoaded() KZ_OVERRIDE;
...
};
//提供处理 XML 的功能。 #include "tinyxml2.h"
//定义用于告诉数据源插件读取哪个 XML 文件的属性类型的元数据。 PropertyType<string> XMLDataSource::XmlFilenameProperty(kzMakeFixedString("XMLdatasource.XMLDataSourceFile"), "", 0, false, KZ_DECLARE_EDITOR_METADATA ( //设置属性名称在 Kanzi Studio 中显示的方式。 metadata.displayName = "XML Data Source File"; //设置属性的工具提示。 metadata.tooltip = "Sets which XML file the data source plugin reads."; //选择用于编辑属性类型的值的编辑器。 // BrowseFileTextEditor 编辑器含有一个文本框,旁边有一个浏览 (Browse) 按钮。 metadata.editor = "BrowseFileTextEditor"; ));
//添加 XML 元素中的类型特性指定类型的数据对象。//从文本参数获取初始值。 DataObjectSharedPtr addDataObject(Domain *domain, const char* type, const char* name, const char* text) { shared_ptr<DataObject> object; //从 int 类型特性创建整数数据对象。 if (type && strcmp(type, "int") == 0) { int value = 0; if (text) { value = atoi(text); } object = make_shared<DataObjectInt>(domain, name, value); } //从浮点和实数类型特性创建浮点数据对象。 else if (type && (strcmp(type, "real") == 0 || strcmp(type, "float") == 0)) { double value = 0; if (text) { value = atof(text); } object = make_shared<DataObjectReal>(domain, name, value); } //从布尔和布尔值类型特性创建布尔值数据对象。 else if (type && (strcmp(type, "bool") == 0 || strcmp(type, "boolean") == 0)) { bool value = false; if (text) { value = (strcmp(text, "true") == 0); } object = make_shared<DataObjectBool>(domain, name, value); } //从字符串类型特性创建一个字符串数据对象。 else if (type && strcmp(type, "string") == 0) { string value; if (text) { value = text; } object = make_shared<DataObjectString>(domain, name, value); } else { //如未设置类型特性,则创建通用数据对象。 //用于创建数据源中的层级。 object = make_shared<DataObject>(domain, name); } return object; }
addDataObject
函数后面,创建一个函数,将内存中的 XML 结构内容转换为数据对象。//将内存中的 XML 结构内容转换为数据对象。使用这些数据对象来//构造数据源的数据对象树。 //第二个参数设置通道放置新数据对象的节点。 //第三个参数将指针设为正在进行变换和语法分析的位置(在内存中的 XML 内,XML 中的子元素中)。 static void addDataObjectsRecursively(Domain* domain, DataObjectSharedPtr parent, const tinyxml2::XMLElement* xml) { //检查 XML 文件中的当前元素是否设置了类型特性。 const tinyxml2::XMLAttribute* typeAttribute = xml->FindAttribute("type"); //获取类型特性的值。 const char* type = 0; if (typeAttribute) { type = typeAttribute->Value(); } //根据类型特性的值创建数据对象。 DataObjectSharedPtr object = addDataObject(domain, type, xml->Name(), xml->GetText()); //将数据对象作为子对象添加到父数据对象。 parent->addChild(object); //遍历 XML 文件中的树,为当前 XML 元素的每个子元素添加数据对象。 for (const tinyxml2::XMLElement* child = xml->FirstChildElement(); child; child = child->NextSiblingElement()) { //递归。 addDataObjectsRecursively(domain, object, child); } }
addDataObject
函数前面,创建一个函数将用户使用 XML Data Source File 属性设置的 XML 文件从硬盘加载到内存中。//声明 addDataObjectsRecursively 函数。
static void addDataObjectsRecursively(Domain* domain, DataObjectSharedPtr object, const tinyxml2::XMLElement* xml);
//解析 XML 文件并从其内容创建数据对象。
void XMLDataSource::parseFile(vector<char> fileData)
{
//清除之前的数据对象树。
m_root.reset();
//从内存解析 XML 文档并释放开放文件。
tinyxml2::XMLDocument doc;
tinyxml2::XMLError error = doc.Parse(fileData.data(), fileData.size());
//如果插件成功加载 XML Data Source File 属性中设置的文件,则创建数据对象。
if (error == tinyxml2::XML_SUCCESS)
{
//获取根 XML 元素。
const tinyxml2::XMLElement* element = doc.RootElement();
//创建数据源的根数据对象。
m_root = make_shared<DataObject>(getDomain(), "Root");
do
{
//填充根数据对象的子数据对象。
addDataObjectsRecursively(getDomain(), m_root, element);
// 处理根 XML 元素的所有兄弟元素。
element = element->NextSiblingElement();
} while (element);
notifyModified();
}
}
parseFile
函数前面,创建一个函数,将用作数据源的文件内容读取进内存。// 将用作数据源的文件内容读取进内存。 vector<char> readFileContents(string_view filename) { ReadOnlyDiskFile file(filename); vector<char> data(static_cast<size_t>(file.size())); file.read(data.data(), data.size()); return data; }
readFileContents
函数后面,执行加载应用程序 kzb 文件后 Kanzi 调用的函数。// Kanzi 在加载应用程序 kzb 文件后调用 onLoaded。 void XMLDataSource::onLoaded() { // 如果不设置 XML Data Source File 属性值,插件将不会执行任何操作。 string filename = getProperty(XmlFilenameProperty); if (!filename.empty()) { // 调用将 XML 文件内容读取进内存的函数,并解析返回值 // 到解析 XML 文件的函数,并从其内容创建数据对象。 parseFile(readFileContents(filename)); } }
本节演示如何通过随时检查用作数据源的文件是否更改来更新数据源。
要更新数据源:
private: //将指针引入数据源的根数据对象。 DataObjectSharedPtr m_root;为
private: //声明启动监控和读取 XML 文件的线程的函数。 void startWorkerThread(string_view filename); //声明停止监控和读取 XML 文件的线程的函数。 void stopWorkerThread(); //声明在线程中被执行用于监控和读取 XML 文件的函数。 //传递文件名的字段串副本,因为它被用于其他线程中。 void workerThreadCallback(string filename); //引入指向数据源根数据对象的指针。 DataObjectSharedPtr m_root; //引入用于监控和读取 XML 文件的线程。 thread m_workerThread; //引入工人线程的退出标志。 atomic<bool> m_workerThreadExitCondition;
//构造函数。 explicit XMLDataSource(Domain* domain, string_view name): DataSource(domain, name) { }为
//构造函数。 explicit XMLDataSource(Domain* domain, string_view name): DataSource(domain, name), m_workerThreadExitCondition(false) { }
create
函数定义的后面,声明析构函数。public: ... //板构函数 ~XMLDataSource();
//提供系统实用工具函数来获取文件的时间戳。 #include <sys/stat.h>
parseFile
函数前面,创建监控和读取用作数据源的文件的函数。//辅助函数,用于获取文件修改时间。 static time_t getFileModificationTime(const char* filename) { time_t result = 0; struct stat fs; if (stat(filename, &fs) == 0) { result = fs.st_mtime; } return result; } //启动监控和读取 XML 文件的线程。 void XMLDataSource::startWorkerThread(string_view filename) { kzAssert(!m_workerThread.joinable()); m_workerThread = thread(&XMLDataSource::workerThreadCallback, this, string(filename)); } //停止监控和读取 XML 文件的线程。 void XMLDataSource::stopWorkerThread() { m_workerThreadExitCondition = true; if (m_workerThread.joinable()) { m_workerThread.join(); } m_workerThreadExitCondition = false; } //此线程函数每秒钟检查一次 XML 文件是否被修改。 //此函数读取 XML 文件并向 UI 线程提交一个任务,将文件内容 //解析进数据源结构中。 void XMLDataSource::workerThreadCallback(string filename) { TaskDispatcher* taskDispatcher = getDomain()->getTaskDispatcher(); //初始化上一次对无效值的修改时间。 time_t oldModificationTime = static_cast<time_t>(-1); for (;;) { if (!filename.empty()) { time_t newModificationTime = getFileModificationTime(filename.c_str()); if (oldModificationTime != newModificationTime) { //将文件内容读取进内存。 //在此线程上读取文件内容,避免阻止 UI 线程长期运行。 vector<char> data = readFileContents(filename); //提交任务,解析 XML 文件并构造数据源。 //必须在 UI 线程中执行此操作,因为只有在 UI 线程中,才可以控制数据源对象 //和其他 UI 对象。 taskDispatcher->submit(bind(&XMLDataSource::parseFile, this, kanzi::move(data))); oldModificationTime = newModificationTime; } } //睡眠一秒钟。 kzsThreadSleep(1000); //如果 UI 线程设有退出标志,则退出。 if (m_workerThreadExitCondition) { break; } } }
onLoaded
函数,添加监控和读取 XML 文件的关闭和启动线程。// Kanzi 在加载应用程序 kzb 文件后调用 onLoaded。 void XMLDataSource::onLoaded() { //每当用户编辑数据源属性或刷新数据源时, //Kanzi Studio 预览 (Preview) 可以多次调用 onLoaded 函数。 //关闭监控和读取 XML 文件的线程。 stopWorkerThread(); ... if (!filename.empty()) { ... //启动监控和读取 XML 文件的线程。 startWorkerThread(filename); } }
onLoaded
函数前面,定义析构函数。//析构函数 XMLDataSource::~XMLDataSource() { //关闭监控和读取 XML 文件的线程。 stopWorkerThread(); }
创建插件之后,构建插件.dll,您可使用该插件在本教程中 Kanzi Studio 工程的下一步中,从 XML 文件获取应用程序数据。
要构建数据源插件,请执行以下操作: